﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace M3G {
    public class VertexBuffer : Object3D {

        #region Field
        bool           immutable;
        VertexArray    positions;
        VertexArray    normals;
        VertexArray    colors;
        VertexArray    pointSizes;
        VertexArray    boneIndices;
        VertexArray    boneWeights;
        VertexArray[]  texCoords;
        uint           defaultColor;
        float          defaultPointSize;
        float          positionsScale;
        float[]        positionsBias;
        float[]        texCoordsScale;
        float[][]      texCoordsBias;
        Dictionary<String, VertexArray> attributes;
        Dictionary<String, bool>        attributeSigned;
        Dictionary<String, bool>        attributeNormalized;
        Dictionary<String, float[]>     attributeDefaultValue;
        #endregion



        #region Method
        public VertexBuffer () {
            this.immutable = false;

            this.positions = null;
            this.normals = null;
            this.colors = null;
            this.pointSizes = null;
            this.boneIndices = null;
            this.boneWeights = null;
            this.texCoords = new VertexArray[] { null, null };

            this.positionsScale = 0;
            this.positionsBias = new float[] { 0, 0, 0 };
            this.texCoordsScale = new float[] { 0, 0 };
            this.texCoordsBias = new float[][]{new float[]{0,0,0},
                                             new float[]{0,0,0}};

            this.defaultColor = 0xffffffff;
            this.defaultPointSize = 1.0f;

            this.attributes = new Dictionary<string, VertexArray> ();
            this.attributeSigned = new Dictionary<string, bool> ();
            this.attributeNormalized = new Dictionary<string, bool> ();
            this.attributeDefaultValue = new Dictionary<string, float[]> ();
        }

        protected VertexBuffer (VertexBuffer vertexBuffer)
            : base (vertexBuffer) {
            this.immutable = vertexBuffer.immutable;
            this.positions = vertexBuffer.positions;
            this.normals = vertexBuffer.normals;
            this.colors = vertexBuffer.colors;
            this.pointSizes = vertexBuffer.pointSizes;
            this.boneIndices = vertexBuffer.boneIndices;
            this.boneWeights = vertexBuffer.boneWeights;
            this.texCoords = (VertexArray[])vertexBuffer.texCoords.Clone ();
            this.defaultColor = vertexBuffer.defaultColor;
            this.defaultPointSize = vertexBuffer.defaultPointSize;
            this.positionsScale = vertexBuffer.positionsScale;
            this.positionsBias = vertexBuffer.positionsBias;
            this.texCoordsScale = (float[])vertexBuffer.texCoordsScale.Clone ();
            this.texCoordsBias = (float[][])vertexBuffer.texCoordsBias.Clone ();
            for (var i=0; i < texCoordsBias.Length; i++) {
                this.texCoordsBias[i] = (float[])vertexBuffer.texCoordsBias[i].Clone ();
            }
            this.attributes = new Dictionary<String, VertexArray> (vertexBuffer.attributes);
            this.attributeSigned = new Dictionary<String, bool> (vertexBuffer.attributeSigned);
            this.attributeNormalized = new Dictionary<String, bool> (vertexBuffer.attributeNormalized);
            this.attributeDefaultValue = new Dictionary<String, float[]> (vertexBuffer.attributeDefaultValue);
        }

        public int VertexCount {
            get {
                return (positions != null) ? positions.VertexCount :
                       (normals != null) ? normals.VertexCount :
                       (colors != null) ? colors.VertexCount :
                       (pointSizes != null) ? pointSizes.VertexCount :
                       (texCoords[0] != null) ? texCoords[0].VertexCount :
                       (texCoords[1] != null) ? texCoords[1].VertexCount : 0;
            }
        }

        public bool Mutable {
            get { return !immutable; }
        }


        public void Commit () {
            this.immutable = true;
        }

        public int AttributeCount {
            get { return attributes.Count; }
        }


        public String[] AttributeNames {
            get {
                return attributes.Keys.ToArray ();
            }
        }

        public VertexArray Attribute (String name, bool[] signedNormalized, float[] defaultValue) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (attributes.ContainsKey (name) == false) {
                throw new ArgumentException ("Attribute does not exist, name=" + name);
            }
            if (signedNormalized != null && signedNormalized.Length < 2 && Attribute (null, null, null) != null
                && (Attribute (name, null, null).ComponentType == ArrayType.Byte || Attribute (name, null, null).ComponentType == ArrayType.Short)) {
                throw new ArgumentException ("Length of SignedNormalized is less than required, len=" + signedNormalized.Length + ",req=2");
            }
            if (defaultValue != null && defaultValue.Length < 4) {
                throw new ArgumentException ("Length of default value is less than required, len=" + defaultValue.Length + ",req=4");
            }

            signedNormalized[0] = false;
            signedNormalized[1] = false;
            if (attributes[name].ComponentType == ArrayType.Byte || attributes[name].ComponentType == ArrayType.Short) {
                signedNormalized[0] = attributeSigned[name];
                signedNormalized[1] = attributeNormalized[name];
            }
            Array.Copy (attributeDefaultValue[name], defaultValue, 4);

            return attributes[name];
        }


        public void SetAttribute (String name, float v0, float v1, float v2, float v3) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (name == "") {
                throw new ArgumentException ("Name is  empty");
            }
            if (name.StartsWith ("m3g_", true, null) || name.StartsWith ("_m3g", true, null)) {
                throw new ArgumentException ("Don't use reserved word(\"m3g_\" or \"_m3g\"(case insentive)), name=" + name);
            }

            this.attributeDefaultValue[name] = new float[] { v0, v1, v2, v3 };
        }

        public void SetAttribute (String name, VertexArray attributes, bool signed, bool normalized) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (immutable == true) {
                throw new ArgumentException ("This VertexBuffer is immutable");
            }
            if (name == "") {
                throw new ArgumentException ("Name is empty");
            }
            if (name.StartsWith ("m3g_", true, null) || name.StartsWith ("_m3g", true, null)) {
                throw new ArgumentException ("Don't use reserved word(\"m3g_\" or \"_m3g\"(case insentive)), name=" + name);
            }
            if (attributes != null && attributes.VertexCount != VertexCount && (positions != null || normals != null || colors != null || texCoords[0] != null || texCoords[1] != null)) {
                throw new ArgumentException ("Vertex count of Attributes must be equal vertex count of VertexBuffer");
            }

            this.attributes[name] = attributes;
            this.attributeSigned[name] = signed;
            this.attributeNormalized[name] = normalized;
        }



        public VertexArray BoneIndices {
            get {
                return boneIndices;
            }
        }

        public VertexArray BoneWeights {
            get {
                return boneWeights;
            }
        }

        public void SetBoneInfluences (VertexArray boneIndices, VertexArray boneWeights) {
            if ((boneIndices == null && boneWeights != null) || (boneIndices != null && boneWeights == null)) {
                throw new ArgumentNullException ("Both indices and weights must be set at once");
            }
            if (immutable == true) {
                throw new InvalidOperationException ("This VertexBuffer is immutable");
            }
            if (boneIndices != null && boneIndices.ComponentType != ArrayType.Byte) {
                throw new ArgumentException ("BoneIndices must be Byte, type=" + boneIndices.ComponentType);
            }
            if (boneIndices != null && boneIndices.VertexCount != boneWeights.VertexCount) {
                throw new ArgumentException ("Vertex count of indices and weights must be equal, i=" + boneIndices.VertexCount + ",w=" + boneWeights.VertexCount);
            }
            if (boneIndices != null && boneIndices.ComponentCount != boneWeights.ComponentCount) {
                throw new ArgumentException ("Component count of indices and weights must be equal, i=" + boneIndices.ComponentCount + ",w=" + boneWeights.ComponentCount);
            }
            if ((boneIndices != null && boneIndices.VertexCount != VertexCount) && (positions != null || normals != null || colors != null || texCoords[0] != null || texCoords[1] != null || pointSizes != null)) {
                throw new ArgumentException ("Vertex count of indices and weigths must be equal to vertex count of this VertexBuffer");
            }

            this.boneIndices = boneIndices;
            this.boneWeights = boneWeights;
        }


        public VertexArray Colors (float[] scaleBias) {
            if (colors == null) {
                return null;
            }

            if (scaleBias != null) {
                float scale;
                switch (colors.ComponentType) {
                    case ArrayType.Byte: scale = 1 / 255.0f; break;
                    case ArrayType.Fixed: scale = 1 / 65536.0f; break;
                    case ArrayType.Float: scale = 1; break;
                    default: scale = 1; break;
                };
                scaleBias[0] = scale;
                scaleBias[1] = 0;
                scaleBias[2] = 0;
                scaleBias[3] = 0;
            }
            return colors;
        }

        public void SetColors (VertexArray colors) {
            if (immutable == true) {
                throw new ArgumentNullException ("This VertexBuffer is immutable");
            }
            if (colors != null && colors.ComponentType != ArrayType.Byte && colors.ComponentType != ArrayType.Fixed &&
                colors.ComponentType != ArrayType.Float) {
                throw new ArgumentException ("Component type of Colors is inalid, type=" + colors.ComponentType);
            }
            if (colors != null && colors.ComponentCount != 3 && colors.ComponentCount != 4) {
                throw new ArgumentException ("Component count of Colors must be 3,4, count=" + colors.ComponentCount);
            }
            if (colors != null && colors.VertexCount != VertexCount && (positions != null || normals != null || texCoords[0] != null || texCoords[1] != null || pointSizes != null)) {
                throw new ArgumentException ("Vertex count of Colors must be equal vertex count of this VertexBuffer");
            }

            this.colors = colors;
        }

        public uint DefaultColor {
            get {
                return defaultColor;
            }
            set {
                this.defaultColor = value;
            }
        }

        public float DefaultPointSize {
            get { return defaultPointSize; }
            set {
                var pointSize = value;
                if (pointSize < 0) {
                    throw new ArgumentException ("PoinSize is invalid, size=" + pointSize);
                }

                this.defaultPointSize = pointSize;
            }

        }



        public VertexArray Normals (float[] scaleBias) {
            if (normals == null) {
                return null;
            }

            if (scaleBias != null) {
                float   scale;
                float[] bias;
                switch (normals.ComponentType) {
                    case ArrayType.Byte:
                    scale = 2 / 255.0f;
                    bias = new float[] {1/255.0f,
                                        1/255.0f,
                                        1/255.0f};
                    break;
                    case ArrayType.Short:
                    scale = 2 / 65535.0f;
                    bias = new float[] {1 / 65535.0f,
                                        1 / 65535.0f,
                                        1 / 65535.0f};
                    break;
                    case ArrayType.Fixed:
                    scale = 1 / 65536.0f;
                    bias = new float[] { 0,
                                         0,
                                         0};
                    break;
                    default:
                    scale = 1;
                    bias = new float[] { 0,
                                             0,
                                             0};
                    break;
                }
                scaleBias[0] = scale;
                scaleBias[1] = bias[0];
                scaleBias[2] = bias[1];
                scaleBias[3] = bias[2];

            }
            return normals;
        }


        public void SetNormals (VertexArray normals) {
            if (immutable == true) {
                throw new InvalidOperationException ("This VertexBuffer is immutable");
            }
            if (normals != null && normals.ComponentCount != 3) {
                throw new ArgumentException ("Component count of Normals must be 3, count=" + normals.ComponentCount);
            }
            if (normals != null && normals.VertexCount != VertexCount && (positions != null || colors != null || texCoords[0] != null || texCoords[1] != null)) {
                throw new ArgumentException ("Vertex count of Normals must be equal to vertex count of this VertexBuffer");
            }

            this.normals = normals;
        }


        public VertexArray PointSizes (float[] scaleBias) {
            if (pointSizes == null) {
                return null;
            }
            if (scaleBias != null && scaleBias.Length < 2) {
                throw new ArgumentException ("Length of scaleBias is less than required, len=" + scaleBias.Length + ",req=2");
            }

            if (scaleBias != null) {
                float scale;
                switch (pointSizes.ComponentType) {
                    case ArrayType.Fixed: scale = 1 / 65536.0f; break;
                    case ArrayType.Float: scale = 1; break;
                    default: scale = 1; break;
                }
                scaleBias[0] = scale;
                scaleBias[1] = 0;
            }

            return pointSizes;
        }


        public void SetPointSizes (VertexArray pointSizes) {
            if (immutable == true) {
                throw new ArgumentException ("This VertexBuffer is immutable");
            }
            if (pointSizes != null && pointSizes.ComponentCount != 1) {
                throw new ArgumentException ("Component count of PointSizes is invalid, count=" + pointSizes.ComponentCount);
            }
            if (pointSizes != null && (pointSizes.ComponentType == ArrayType.Byte || pointSizes.ComponentType == ArrayType.Short)) {
                throw new ArgumentException ("Component type of PointSizes is invalid, type=" + pointSizes.ComponentType);
            }
            if (pointSizes != null && pointSizes.VertexCount != VertexCount && (positions != null || normals != null || colors != null || texCoords[0] != null || texCoords[1] != null)) {
                throw new ArgumentException ("Vertex count of vertex count is equal to vertex count of this VertexBuffer");
            }

            this.pointSizes = pointSizes;
        }


        public VertexArray Positions (float[] scaleBias) {
            if (positions == null) {
                return null;
            }
            var biasCount = Math.Min (3, positions.ComponentCount);
            var required =  biasCount + 1;
            if (scaleBias != null && scaleBias.Length < required) {
                throw new ArgumentException ("Length of scaleBias is less than required, len=" + scaleBias.Length + ",req=" + required);
            }
            if (scaleBias != null) {
                scaleBias[0] = positionsScale;
                for (int i=0; i < biasCount; i++) {
                    scaleBias[i + 1] = positionsBias[i];
                }
            }
            return positions;
        }



        public void SetPositions (VertexArray positions, float scale, float[] bias) {
            if (immutable == true) {
                throw new InvalidOperationException ("This VertexBuffer is immutable");
            }
            if (positions != null && (positions.ComponentCount < 2 || positions.ComponentCount > 4)) {
                throw new ArgumentException ("Component count of Positions is 2,3,4, count=" + positions.ComponentCount);
            }
            if (positions != null && positions.VertexCount != VertexCount && (normals != null || colors != null || pointSizes != null || texCoords[0] != null || texCoords[1] != null)) {
                throw new ArgumentException ("Vertex count of Positions must be equal to vertex count of this VertexBuffer");
            }
            var biasCount = Math.Min (3, positions.ComponentCount);
            if (positions != null && bias != null && bias.Length < biasCount) {
                throw new ArgumentException ("Length of bias is invalid, len=" + bias.Length);
            }

            this.positions = positions;
            this.positionsScale = scale;
            for (var i=0; i < biasCount; i++) {
                this.positionsBias[i] = bias[i];
            }
        }


        public VertexArray TexCoords (int index, float[] scaleBias) {
            if (index < 0 || index > 1) {
                throw new ArgumentOutOfRangeException ("Texture index is out of range, index=" + index);
            }
            if (texCoords[index] == null) {
                return null;
            }
            var biasCount = Math.Min (3, texCoords[index].ComponentCount);
            var required = biasCount + 1;
            if (scaleBias != null && scaleBias.Length < required) {
                throw new ArgumentException ("Length of scaleBias is less than required, len=" + scaleBias.Length + ",req=" + required);
            }

            if (scaleBias != null) {
                scaleBias[0] = texCoordsScale[index];
                for (var i=0; i < biasCount; i++) {
                    scaleBias[i + 1] = texCoordsBias[index][i];
                }
            }

            return texCoords[index];
        }

        public void SetTexCoords (int index, VertexArray texCoords, float scale, float[] bias) {
            if (immutable == true) {
                throw new InvalidOperationException ("This VertexBuffer is immutable");
            }
            if (texCoords != null && (texCoords.ComponentCount < 2 || texCoords.ComponentCount > 4)) {
                throw new ArgumentException ("Component count of TexCoords is invalid, count=" + texCoords.ComponentCount);
            }
            if (texCoords != null && texCoords.VertexCount != VertexCount && (positions != null || normals != null || colors != null || pointSizes != null)) {
                throw new ArgumentException ("Vertex count of TexCoords must be equal vertex count of this VertexBuffer");
            }
            var biasCount = Math.Min (3, texCoords.ComponentCount);
            if (texCoords != null && bias != null && bias.Length < biasCount) {
                throw new ArgumentException ("Length of bias is less than required, len=" + bias.Length);
            }
            if (index < 0 || index > 2) {
                throw new ArgumentOutOfRangeException ("Index is out of range, index=" + index);
            }

            this.texCoords[index] = texCoords;
            this.texCoordsScale[index] = scale;
            for (var i=0; i < biasCount; i++) {
                this.texCoordsBias[index][i] = bias[i];
            }
        }

        public override Object3D[] References {
            get {
                var q = new List<Object3D> ();
                q.AddRange (base.References);
                q.Add (positions);
                q.Add (normals);
                q.Add (colors);
                q.Add (pointSizes);
                q.Add (boneIndices);
                q.Add (boneWeights);
                q.AddRange (texCoords);
                q.AddRange (attributes.Values);
                q.RemoveAll (x => x == null);
                return q.ToArray ();
            }
        }

        public override int Animate (int time) {

            base.Animate (time);

            var animatedAlpha     = new float[1];
            var animatedColor     = new float[3];
            var animatedPointSize = new float[1];
            var value             = new float[3];
            var animated          = new Dictionary<int, bool> (3);

            for (var i=0; i < AnimationTrackCount; i++) {
                var track = AnimationTrack[i];
                var ch    = AnimationTrackChannel[i];
                if (track.Controller == null || !track.Controller.Active (time)) {
                    continue;
                }
                var property = track.TargetProperty;
                var weight   = track.Controller.Weight;

                animated[property] = true;
                switch (property) {
                    case M3G.AnimationTrack.Alpha: {
                        track.Sample (time, ch, value);
                        animatedAlpha[0] = value[0] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.Color: {
                        track.Sample (time, ch, value);
                        animatedColor[0] = value[0] * weight;
                        animatedColor[1] = value[1] * weight;
                        animatedColor[2] = value[2] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.PointSize: {
                        track.Sample (time, ch, value);
                        animatedPointSize[0] = value[0] * weight;
                        break;
                    }
                }// switch
            }  // for

            if (animated[M3G.AnimationTrack.Alpha]) {
                var a = (uint)animatedAlpha[0].Clamp (0, 1) * 255;
                this.defaultColor = (defaultColor & 0x00ffffff) | (a << 24);
            }
            if (animated[M3G.AnimationTrack.Color]) {
                var r = (uint)animatedColor[0].Clamp (0, 1) * 255;
                var g = (uint)animatedColor[1].Clamp (0, 1) * 255;
                var b = (uint)animatedColor[2].Clamp (0, 1) * 255;
                this.defaultColor = (defaultColor & 0xff000000) | (r << 16) | (g << 8) | (b << 0);
            }
            if (animated[M3G.AnimationTrack.Stencil]) {
                this.defaultPointSize = (int)animatedPointSize[0];
            }

            return 0;
        }

        public override Object3D Duplicate () {
            return new VertexBuffer (this);
        }
        #endregion
    }
}
